// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/update_dependency_command.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';

import 'mocks.dart';
import 'util.dart';

void main() {
  late Directory packagesDir;
  late RecordingProcessRunner processRunner;
  late CommandRunner<void> runner;
  Future<http.Response> Function(http.Request request)? mockHttpResponse;

  setUp(() {
    final GitDir gitDir;
    (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) =
        configureBaseCommandMocks();
    final command = UpdateDependencyCommand(
      packagesDir,
      processRunner: processRunner,
      gitDir: gitDir,
      httpClient: MockClient(
        (http.Request request) => mockHttpResponse!(request),
      ),
    );

    runner = CommandRunner<void>(
      'update_dependency_command',
      'Test for update-dependency command.',
    );
    runner.addCommand(command);
  });

  /// Adds a dummy 'dependencies:' entries for [dependency] to [package].
  void addDependency(
    RepositoryPackage package,
    String dependency, {
    String version = '^1.0.0',
  }) {
    final List<String> lines = package.pubspecFile.readAsLinesSync();
    final int dependenciesStartIndex = lines.indexOf('dependencies:');
    assert(dependenciesStartIndex != -1);
    lines.insert(dependenciesStartIndex + 1, '  $dependency: $version');
    package.pubspecFile.writeAsStringSync(lines.join('\n'));
  }

  /// Adds a 'dev_dependencies:' section with an entry for [dependency] to
  /// [package].
  void addDevDependency(
    RepositoryPackage package,
    String dependency, {
    String version = '^1.0.0',
  }) {
    final String originalContent = package.pubspecFile.readAsStringSync();
    package.pubspecFile.writeAsStringSync('''
$originalContent

dev_dependencies:
  $dependency: $version
''');
  }

  test('throws if no target is provided', () async {
    Error? commandError;
    final List<String> output = await runCapturingPrint(
      runner,
      <String>['update-dependency'],
      errorHandler: (Error e) {
        commandError = e;
      },
    );

    expect(commandError, isA<ToolExit>());
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('Exactly one of the target flags must be provided:'),
      ]),
    );
  });

  test('throws if multiple dependencies specified', () async {
    Error? commandError;
    final List<String> output = await runCapturingPrint(
      runner,
      <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--android-dependency',
        'gradle',
      ],
      errorHandler: (Error e) {
        commandError = e;
      },
    );

    expect(commandError, isA<ToolExit>());
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('Exactly one of the target flags must be provided:'),
      ]),
    );
  });

  group('pub dependencies', () {
    test('throws if no version is given for an unpublished target', () async {
      mockHttpResponse = (http.Request request) async {
        return http.Response('', 404);
      };

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>['update-dependency', '--pub-package', 'target_package'],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('target_package does not exist on pub'),
        ]),
      );
    });

    test('skips if there is no dependency', () async {
      createFakePackage('a_package', packagesDir, examples: <String>[]);

      final List<String> output = await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('SKIPPING: Does not depend on target_package'),
        ]),
      );
    });

    test('skips if the dependency is already the target version', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package', version: '^1.5.0');

      final List<String> output = await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('SKIPPING: Already depends on ^1.5.0'),
        ]),
      );
    });

    test('logs updates', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package');

      final List<String> output = await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      expect(
        output,
        containsAllInOrder(<Matcher>[contains('Updating to "^1.5.0"')]),
      );
    });

    test('updates normal dependency', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '^1.5.0');
    });

    test('updates dev dependency', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDevDependency(package, 'target_package');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .devDependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '^1.5.0');
    });

    test('updates dependency in example', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
      );
      final RepositoryPackage example = package.getExamples().first;
      addDevDependency(example, 'target_package');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      final Dependency? dep = example
          .parsePubspec()
          .devDependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '^1.5.0');
    });

    test('uses provided constraint as-is', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package');

      const providedConstraint = '>=1.6.0 <3.0.0';
      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        providedConstraint,
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), providedConstraint);
    });

    test('uses provided version as lower bound for unpinned', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '^1.5.0');
    });

    test('uses provided version as exact version for pinned', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package', version: '1.0.0');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
        '--version',
        '1.5.0',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '1.5.0');
    });

    test('uses latest pub version as lower bound for unpinned', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package');

      const targetPackagePubResponse = <String, dynamic>{
        'name': 'a',
        'versions': <String>['0.0.1', '1.0.0', '1.5.0'],
      };
      mockHttpResponse = (http.Request request) async {
        if (request.url.pathSegments.last == 'target_package.json') {
          return http.Response(json.encode(targetPackagePubResponse), 200);
        }
        return http.Response('', 500);
      };

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '^1.5.0');
    });

    test('uses latest pub version as exact version for pinned', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        examples: <String>[],
      );
      addDependency(package, 'target_package', version: '1.0.0');

      const targetPackagePubResponse = <String, dynamic>{
        'name': 'a',
        'versions': <String>['0.0.1', '1.0.0', '1.5.0'],
      };
      mockHttpResponse = (http.Request request) async {
        if (request.url.pathSegments.last == 'target_package.json') {
          return http.Response(json.encode(targetPackagePubResponse), 200);
        }
        return http.Response('', 500);
      };

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'target_package',
      ]);

      final Dependency? dep = package
          .parsePubspec()
          .dependencies['target_package'];
      expect(dep, isA<HostedDependency>());
      expect((dep! as HostedDependency).version.toString(), '1.5.0');
    });

    test('regenerates all pigeon files when updating pigeon', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        extraFiles: <String>['pigeons/foo.dart', 'pigeons/bar.dart'],
      );
      addDependency(package, 'pigeon', version: '1.0.0');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'pigeon',
        '--version',
        '1.5.0',
      ]);

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('dart', const <String>['pub', 'get'], package.path),
          ProcessCall('dart', const <String>[
            'run',
            'pigeon',
            '--input',
            'pigeons/foo.dart',
          ], package.path),
          ProcessCall('dart', const <String>[
            'run',
            'pigeon',
            '--input',
            'pigeons/bar.dart',
          ], package.path),
        ]),
      );
    });

    test(
      'warns when regenerating pigeon if there are no pigeon files',
      () async {
        final RepositoryPackage package = createFakePackage(
          'a_package',
          packagesDir,
        );
        addDependency(package, 'pigeon', version: '1.0.0');

        final List<String> output = await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--pub-package',
          'pigeon',
          '--version',
          '1.5.0',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('No pigeon input files found'),
          ]),
        );
      },
    );

    test('updating pigeon fails if pub get fails', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        extraFiles: <String>['pigeons/foo.dart'],
      );
      addDependency(package, 'pigeon', version: '1.0.0');

      processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'update-dependency',
          '--pub-package',
          'pigeon',
          '--version',
          '1.5.0',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Fetching dependencies failed'),
          contains('Failed to update pigeon files'),
        ]),
      );
    });

    test('updating pigeon fails if running pigeon fails', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
        extraFiles: <String>['pigeons/foo.dart'],
      );
      addDependency(package, 'pigeon', version: '1.0.0');

      processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'pigeon']),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'update-dependency',
          '--pub-package',
          'pigeon',
          '--version',
          '1.5.0',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('dart run pigeon failed'),
          contains('Failed to update pigeon files'),
        ]),
      );
    });

    test('regenerates mocks when updating mockito if necessary', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
      );
      addDependency(package, 'mockito', version: '1.0.0');
      addDevDependency(package, 'build_runner');

      await runCapturingPrint(runner, <String>[
        'update-dependency',
        '--pub-package',
        'mockito',
        '--version',
        '1.5.0',
      ]);

      expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('dart', const <String>['pub', 'get'], package.path),
          ProcessCall('dart', const <String>[
            'run',
            'build_runner',
            'build',
            '--delete-conflicting-outputs',
          ], package.path),
        ]),
      );
    });

    test(
      'skips regenerating mocks when there is no build_runner dependency',
      () async {
        final RepositoryPackage package = createFakePackage(
          'a_package',
          packagesDir,
        );
        addDependency(package, 'mockito', version: '1.0.0');

        await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--pub-package',
          'mockito',
          '--version',
          '1.5.0',
        ]);

        expect(processRunner.recordedCalls.isEmpty, true);
      },
    );

    test('updating mockito fails if pub get fails', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
      );
      addDependency(package, 'mockito', version: '1.0.0');
      addDevDependency(package, 'build_runner');

      processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'update-dependency',
          '--pub-package',
          'mockito',
          '--version',
          '1.5.0',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('Fetching dependencies failed'),
          contains('Failed to update mocks'),
        ]),
      );
    });

    test('updating mockito fails if running build_runner fails', () async {
      final RepositoryPackage package = createFakePackage(
        'a_package',
        packagesDir,
      );
      addDependency(package, 'mockito', version: '1.0.0');
      addDevDependency(package, 'build_runner');

      processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
        FakeProcessInfo(MockProcess(), <String>['pub', 'get']),
        FakeProcessInfo(MockProcess(exitCode: 1), <String>[
          'run',
          'build_runner',
        ]),
      ];

      Error? commandError;
      final List<String> output = await runCapturingPrint(
        runner,
        <String>[
          'update-dependency',
          '--pub-package',
          'mockito',
          '--version',
          '1.5.0',
        ],
        errorHandler: (Error e) {
          commandError = e;
        },
      );

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains('"dart run build_runner build" failed'),
          contains('Failed to update mocks'),
        ]),
      );
    });
  });

  group('Android dependencies', () {
    final invalidGradleAgpVersionsFormat = <String>[
      '81',
      '811.1',
      '8.123',
      '8.12.12',
    ];

    const invalidGradleAgpVersionError = '''
A version with a valid format (maximum 2-3 numbers separated by 1-2 periods) must be provided.
            1. The first number must have one or two digits
            2. The second number must have one or two digits
            3. If present, the third number must have a single digit''';

    const invalidKgpVersionError = '''
A version with a valid format (3 numbers separated by 2 periods) must be provided.
            1. The first number must have one digit
            2. The second number must have one digit
            3. The third number must have one or two digits''';

    group('gradle', () {
      for (final gradleVersion in invalidGradleAgpVersionsFormat) {
        test(
          'throws because gradleVersion: $gradleVersion is invalid',
          () async {
            Error? commandError;
            final List<String> output = await runCapturingPrint(
              runner,
              <String>[
                'update-dependency',
                '--android-dependency',
                'gradle',
                '--version',
                gradleVersion,
              ],
              errorHandler: (Error e) {
                commandError = e;
              },
            );

            expect(commandError, isA<ToolExit>());
            expect(
              output,
              containsAllInOrder(<Matcher>[
                contains(invalidGradleAgpVersionError),
              ]),
            );
          },
        );
      }

      test('skips if example app does not run on Android', () async {
        final RepositoryPackage package = createFakePlugin(
          'fake_plugin',
          packagesDir,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--packages',
          package.displayName,
          '--android-dependency',
          'gradle',
          '--version',
          '8.8.8',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('SKIPPING: No example apps run on Android.'),
          ]),
        );
      });

      test(
        'throws if wrapper does not have distribution URL with expected format',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'example/android/app/gradle/wrapper/gradle-wrapper.properties',
            ],
          );

          final File gradleWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          gradleWrapperPropertiesFile.writeAsStringSync('''
How is it even possible that I didn't specify a Gradle distribution?
''');

          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--packages',
              package.displayName,
              '--android-dependency',
              'gradle',
              '--version',
              '8.8.8',
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[
              contains(
                'Unable to find a gradle version entry to update for ${package.displayName}/example.',
              ),
            ]),
          );
        },
      );

      test(
        'succeeds if example app has android/app/gradle directory structure',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'example/android/app/gradle/wrapper/gradle-wrapper.properties',
            ],
          );
          const newGradleVersion = '8.8.8';

          final File gradleWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          gradleWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'gradle',
            '--version',
            newGradleVersion,
          ]);

          final String updatedGradleWrapperPropertiesContents =
              gradleWrapperPropertiesFile.readAsStringSync();
          expect(
            updatedGradleWrapperPropertiesContents,
            contains(
              r'distributionUrl=https\://services.gradle.org/distributions/'
              'gradle-$newGradleVersion-all.zip',
            ),
          );
        },
      );

      test(
        'succeeds if example app has android/gradle directory structure',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'example/android/gradle/wrapper/gradle-wrapper.properties',
            ],
          );
          const newGradleVersion = '9.9';

          final File gradleWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          gradleWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'gradle',
            '--version',
            newGradleVersion,
          ]);

          final String updatedGradleWrapperPropertiesContents =
              gradleWrapperPropertiesFile.readAsStringSync();
          expect(
            updatedGradleWrapperPropertiesContents,
            contains(
              r'distributionUrl=https\://services.gradle.org/distributions/'
              'gradle-$newGradleVersion-all.zip',
            ),
          );
        },
      );

      test(
        'succeeds if example app has android/gradle and android/app/gradle directory structure',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'example/android/gradle/wrapper/gradle-wrapper.properties',
              'example/android/app/gradle/wrapper/gradle-wrapper.properties',
            ],
          );
          const newGradleVersion = '9.9';

          final File gradleWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          final File gradleAppWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          gradleWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');

          gradleAppWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'gradle',
            '--version',
            newGradleVersion,
          ]);

          final String updatedGradleWrapperPropertiesContents =
              gradleWrapperPropertiesFile.readAsStringSync();
          final String updatedGradleAppWrapperPropertiesContents =
              gradleAppWrapperPropertiesFile.readAsStringSync();
          expect(
            updatedGradleWrapperPropertiesContents,
            contains(
              r'distributionUrl=https\://services.gradle.org/distributions/'
              'gradle-$newGradleVersion-all.zip',
            ),
          );
          expect(
            updatedGradleAppWrapperPropertiesContents,
            contains(
              r'distributionUrl=https\://services.gradle.org/distributions/'
              'gradle-$newGradleVersion-all.zip',
            ),
          );
        },
      );

      test(
        'succeeds if one example app runs on Android and another does not',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            examples: <String>['example_1', 'example_2'],
            extraFiles: <String>[
              'example/example_2/android/app/gradle/wrapper/gradle-wrapper.properties',
            ],
          );
          const newGradleVersion = '8.8.8';

          final File gradleWrapperPropertiesFile = package.directory
              .childDirectory('example')
              .childDirectory('example_2')
              .childDirectory('android')
              .childDirectory('app')
              .childDirectory('gradle')
              .childDirectory('wrapper')
              .childFile('gradle-wrapper.properties');

          gradleWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'gradle',
            '--version',
            newGradleVersion,
          ]);

          final String updatedGradleWrapperPropertiesContents =
              gradleWrapperPropertiesFile.readAsStringSync();
          expect(
            updatedGradleWrapperPropertiesContents,
            contains(
              r'distributionUrl=https\://services.gradle.org/distributions/'
              'gradle-$newGradleVersion-all.zip',
            ),
          );
        },
      );
    });
    group('agp', () {
      for (final agpVersion in invalidGradleAgpVersionsFormat) {
        test('throws because agpVersion: $agpVersion is invalid', () async {
          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--android-dependency',
              'androidGradlePlugin',
              '--version',
              agpVersion,
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[
              contains(invalidGradleAgpVersionError),
            ]),
          );
        });
      }

      test('skips if example app does not run on Android', () async {
        final RepositoryPackage package = createFakePlugin(
          'fake_plugin',
          packagesDir,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--packages',
          package.displayName,
          '--android-dependency',
          'androidGradlePlugin',
          '--version',
          '8.11.1',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('SKIPPING: No example apps run on Android.'),
          ]),
        );
      });

      test(
        'succeeds if example app has android/settings.gradle structure',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>['example/android/settings.gradle'],
          );
          const newAgpVersion = '9.9';

          final File gradleSettingsFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childFile('settings.gradle');

          gradleSettingsFile.writeAsStringSync(r'''
...
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version "8.11.1" apply false
...
}
...
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'androidGradlePlugin',
            '--version',
            newAgpVersion,
          ]);

          final String updatedGradleSettingsContents = gradleSettingsFile
              .readAsStringSync();

          expect(
            updatedGradleSettingsContents,
            contains(
              r'id "com.android.application" version '
              '"$newAgpVersion" apply false',
            ),
          );
        },
      );

      test(
        'succeeds if one example app runs on Android and another does not',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            examples: <String>['example_1', 'example_2'],
            extraFiles: <String>['example/example_2/android/settings.gradle'],
          );
          const newAgpVersion = '9.9';

          final File gradleSettingsFile = package.directory
              .childDirectory('example')
              .childDirectory('example_2')
              .childDirectory('android')
              .childFile('settings.gradle');

          gradleSettingsFile.writeAsStringSync(r'''
...
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version "8.11.1" apply false
...
}
...
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'androidGradlePlugin',
            '--version',
            newAgpVersion,
          ]);

          final String updatedGradleSettingsContents = gradleSettingsFile
              .readAsStringSync();

          expect(
            updatedGradleSettingsContents,
            contains(
              r'id "com.android.application" version '
              '"$newAgpVersion" apply false',
            ),
          );
        },
      );
    });
    group('kgp', () {
      final invalidKgpVersionsFormat = <String>[
        '81',
        '81.1',
        '8.123',
        '8.12.12',
        '8.12.1',
      ];

      for (final kgpVersion in invalidKgpVersionsFormat) {
        test('throws because kgpVersion: $kgpVersion is invalid', () async {
          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--android-dependency',
              'kotlinGradlePlugin',
              '--version',
              kgpVersion,
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[contains(invalidKgpVersionError)]),
          );
        });
      }

      test('skips if example app does not run on Android', () async {
        final RepositoryPackage package = createFakePlugin(
          'fake_plugin',
          packagesDir,
        );

        final List<String> output = await runCapturingPrint(
          runner,
          <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'kotlinGradlePlugin',
            '--version',
            '2.2.20',
          ],
          errorHandler: (Error e) {
            print((e as ToolExit).stackTrace);
          },
        );

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('SKIPPING: No example apps run on Android.'),
          ]),
        );
      });

      test(
        'succeeds if example app has android/settings.gradle structure',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>['example/android/settings.gradle'],
          );
          const newKgpVersion = '2.2.20';

          final File gradleSettingsFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childFile('settings.gradle');

          gradleSettingsFile.writeAsStringSync(r'''
...
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    ...
    id "org.jetbrains.kotlin.android" version "2.2.20" apply false
...
}
...
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'kotlinGradlePlugin',
            '--version',
            newKgpVersion,
          ]);

          final String updatedGradleSettingsContents = gradleSettingsFile
              .readAsStringSync();

          expect(
            updatedGradleSettingsContents,
            contains(
              r'    id "org.jetbrains.kotlin.android" version '
              '"$newKgpVersion" apply false',
            ),
          );
        },
      );

      test(
        'succeeds if one example app runs on Android and another does not',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            examples: <String>['example_1', 'example_2'],
            extraFiles: <String>['example/example_2/android/settings.gradle'],
          );
          const newKgpVersion = '2.2.20';

          final File gradleSettingsFile = package.directory
              .childDirectory('example')
              .childDirectory('example_2')
              .childDirectory('android')
              .childFile('settings.gradle');

          gradleSettingsFile.writeAsStringSync(r'''
...
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    ...
    id "org.jetbrains.kotlin.android" version "2.2.20" apply false
...
}
...
''');

          await runCapturingPrint(runner, <String>[
            'update-dependency',
            '--packages',
            package.displayName,
            '--android-dependency',
            'kotlinGradlePlugin',
            '--version',
            newKgpVersion,
          ]);

          final String updatedGradleSettingsContents = gradleSettingsFile
              .readAsStringSync();

          expect(
            updatedGradleSettingsContents,
            contains(
              r'    id "org.jetbrains.kotlin.android" version '
              '"$newKgpVersion" apply false',
            ),
          );
        },
      );
    });

    group('compileSdk/compileSdkForExamples', () {
      // Tests if the compileSdk version is updated for the provided
      // build.gradle file and new compileSdk version to update to.
      Future<void> testCompileSdkVersionUpdated({
        required RepositoryPackage package,
        required File buildGradleFile,
        required String oldCompileSdkVersion,
        required String newCompileSdkVersion,
        bool runForExamples = false,
        bool checkForDeprecatedCompileSdkVersion = false,
      }) async {
        buildGradleFile.writeAsStringSync('''
android {
    // Conditional for compatibility with AGP <4.2.
    if (project.android.hasProperty("namespace")) {
        namespace 'io.flutter.plugins.pathprovider'
    }
    ${checkForDeprecatedCompileSdkVersion ? 'compileSdkVersion' : 'compileSdk'} $oldCompileSdkVersion
''');

        await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--packages',
          package.displayName,
          '--android-dependency',
          if (runForExamples) 'compileSdkForExamples' else 'compileSdk',
          '--version',
          newCompileSdkVersion,
        ]);

        final String updatedBuildGradleContents = buildGradleFile
            .readAsStringSync();
        // compileSdkVersion is now deprecated, so if the tool finds any
        // instances of compileSdk OR compileSdkVersion, it should change it
        // to compileSdk. See https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/CommonExtension#compileSdkVersion(kotlin.Int).
        expect(
          updatedBuildGradleContents,
          contains('compileSdk $newCompileSdkVersion'),
        );
      }

      test('throws if version format is invalid for compileSdk', () async {
        Error? commandError;
        final List<String> output = await runCapturingPrint(
          runner,
          <String>[
            'update-dependency',
            '--android-dependency',
            'compileSdk',
            '--version',
            '834',
          ],
          errorHandler: (Error e) {
            commandError = e;
          },
        );

        expect(commandError, isA<ToolExit>());
        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains(
              'A valid Android SDK version number (1-2 digit numbers) must be provided.',
            ),
          ]),
        );
      });

      test(
        'throws if version format is invalid for compileSdkForExamples',
        () async {
          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--android-dependency',
              'compileSdkForExamples',
              '--version',
              '438',
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[
              contains(
                'A valid Android SDK version number (1-2 digit numbers) must be provided.',
              ),
            ]),
          );
        },
      );

      test('skips if plugin does not run on Android', () async {
        final RepositoryPackage package = createFakePlugin(
          'fake_plugin',
          packagesDir,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--packages',
          package.displayName,
          '--android-dependency',
          'compileSdk',
          '--version',
          '34',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains(
              'SKIPPING: Package ${package.displayName} does not run on Android.',
            ),
          ]),
        );
      });

      test('skips if plugin example does not run on Android', () async {
        final RepositoryPackage package = createFakePlugin(
          'fake_plugin',
          packagesDir,
        );

        final List<String> output = await runCapturingPrint(runner, <String>[
          'update-dependency',
          '--packages',
          package.displayName,
          '--android-dependency',
          'compileSdkForExamples',
          '--version',
          '34',
        ]);

        expect(
          output,
          containsAllInOrder(<Matcher>[
            contains('SKIPPING: No example apps run on Android.'),
          ]),
        );
      });

      test(
        'throws if build configuration file does not have compileSdk version with expected format for compileSdk',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>['android/build.gradle'],
          );

          final File buildGradleFile = package.directory
              .childDirectory('android')
              .childFile('build.gradle');

          buildGradleFile.writeAsStringSync('''
How is it even possible that I didn't specify a compileSdk version?
''');

          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--packages',
              package.displayName,
              '--android-dependency',
              'compileSdk',
              '--version',
              '34',
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[
              contains(
                'Unable to find a compileSdk version entry to update for ${package.displayName}.',
              ),
            ]),
          );
        },
      );

      test(
        'throws if build configuration file does not have compileSdk version with expected format for compileSdkForExamples',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>['example/android/app/build.gradle'],
          );

          final File buildGradleFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childFile('build.gradle');

          buildGradleFile.writeAsStringSync('''
How is it even possible that I didn't specify a compileSdk version?
''');

          Error? commandError;
          final List<String> output = await runCapturingPrint(
            runner,
            <String>[
              'update-dependency',
              '--packages',
              package.displayName,
              '--android-dependency',
              'compileSdkForExamples',
              '--version',
              '34',
            ],
            errorHandler: (Error e) {
              commandError = e;
            },
          );

          expect(commandError, isA<ToolExit>());
          expect(
            output,
            containsAllInOrder(<Matcher>[
              contains(
                'Unable to find a compileSdkForExamples version entry to update for ${package.displayName}/example.',
              ),
            ]),
          );
        },
      );

      test(
        'succeeds if plugin runs on Android and valid version is supplied for compileSdkVersion entry',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'android/build.gradle',
              'example/android/app/build.gradle',
            ],
          );
          final File buildGradleFile = package.directory
              .childDirectory('android')
              .childFile('build.gradle');

          await testCompileSdkVersionUpdated(
            package: package,
            buildGradleFile: buildGradleFile,
            oldCompileSdkVersion: '8',
            newCompileSdkVersion: '16',
            checkForDeprecatedCompileSdkVersion: true,
          );
        },
      );

      test(
        'succeeds if plugin example runs on Android and valid version is supplied for compileSdkVersion entry',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'android/build.gradle',
              'example/android/app/build.gradle',
            ],
          );
          final File exampleBuildGradleFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childFile('build.gradle');

          await testCompileSdkVersionUpdated(
            package: package,
            buildGradleFile: exampleBuildGradleFile,
            oldCompileSdkVersion: '8',
            newCompileSdkVersion: '16',
            runForExamples: true,
            checkForDeprecatedCompileSdkVersion: true,
          );
        },
      );

      test(
        'succeeds if plugin runs on Android and valid version is supplied for compileSdk entry',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'android/build.gradle',
              'example/android/app/build.gradle',
            ],
          );

          final File buildGradleFile = package.directory
              .childDirectory('android')
              .childFile('build.gradle');
          await testCompileSdkVersionUpdated(
            package: package,
            buildGradleFile: buildGradleFile,
            oldCompileSdkVersion: '8',
            newCompileSdkVersion: '16',
          );
        },
      );

      test(
        'succeeds if plugin example runs on Android and valid version is supplied for compileSdk entry',
        () async {
          final RepositoryPackage package = createFakePlugin(
            'fake_plugin',
            packagesDir,
            extraFiles: <String>[
              'android/build.gradle',
              'example/android/app/build.gradle',
            ],
          );

          final File exampleBuildGradleFile = package.directory
              .childDirectory('example')
              .childDirectory('android')
              .childDirectory('app')
              .childFile('build.gradle');
          await testCompileSdkVersionUpdated(
            package: package,
            buildGradleFile: exampleBuildGradleFile,
            oldCompileSdkVersion: '33',
            newCompileSdkVersion: '34',
            runForExamples: true,
          );
        },
      );
    });
  });
}
